; Inno Setup 脚本 - LDNS 安装程序
; 作者: huobing

#define MyAppName "LDNS"
#define MyAppExeName "LDNS.exe"
#define MyAppAssocName "LDNS"
#define MyAppAssocExt ".exe"
#define MyAppDesc "DNS服务应用程序"
#define MyAppCopyright "Copyright © huobing 2025"
#define MyAppPublisher GetStringFileInfo("bin\\Release\\net8.0\\win-x64-no-runtime\\LDNS.exe", "CompanyName")
#define MyAppURL GetStringFileInfo("bin\\Release\\net8.0\\win-x64-no-runtime\\LDNS.exe", "LegalTrademarks")
#ifndef MyAppVersion
#define MyAppVersion GetStringFileInfo("bin\\Release\\net8.0\\win-x64-no-runtime\\LDNS.exe", "ProductVersion")
#endif

[Setup]
; 应用程序基本信息
AppId={{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
AppCopyright={#MyAppCopyright}
; 根据系统环境自动选择默认安装目录
DefaultDirName={code:GetDefaultInstallationDir}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
OutputDir=bin\Release
OutputBaseFilename=LDNS-Setup-{#MyAppVersion}
SetupIconFile=Ico\256.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=admin
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; 显示进度条详情
UseSetupLdr=yes
ShowLanguageDialog=yes

; 安装程序语言
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

; 安装程序任务
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "installservice"; Description: "安装为Windows服务（开机自启动）"; GroupDescription: "启动选项"; Flags: CheckedOnce

; 安装程序文件
[Files]
; 使用发布输出的框架依赖目录，包含所有所需文件
Source: "bin\Release\net8.0\win-x64-no-runtime\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

; 保留文档与图标
Source: "docs\*"; DestDir: "{app}\docs"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Conf\*"; DestDir: "{app}\Conf"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Ico\256.ico"; DestDir: "{app}"; Flags: ignoreversion

; 创建 ProgramData 日志目录并授予普通用户写入权限，避免 Program Files 目录写入失败
[Dirs]
Name: "{commonappdata}\LDNS\logs"; Permissions: users-modify; Flags: uninsalwaysuninstall
Name: "{commonappdata}\LDNS\logs\archive"; Permissions: users-modify; Flags: uninsalwaysuninstall

; 开始菜单快捷方式
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\256.ico"
Name: "{commondesktop}\LDNS.UI"; Filename: "{app}\LDNS.UI.exe"; IconFilename: "{app}\256.ico"; Tasks: desktopicon
Name: "{group}\LDNS.UI"; Filename: "{app}\LDNS.UI.exe"; IconFilename: "{app}\256.ico"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"

; 卸载信息
[UninstallDelete]
Type: filesandordirs; Name: "{app}\Conf"
Type: filesandordirs; Name: "{app}\Logs"
Type: files; Name: "{app}\Logs\*.log"

; 自定义安装程序代码
[Code]
// 获取默认安装目录（注意：用于DefaultDirName的函数必须接受一个参数）
function GetDefaultInstallationDir(Value: string): string;
var
  ToolsDir: string;
begin
  ToolsDir := 'D:\Tools\{#MyAppName}';
  // 检查D:\Tools目录是否存在
  if DirExists('D:\Tools') then
  begin
    Result := ToolsDir;
  end
  else
  begin
    // 使用默认的Program Files目录
    Result := ExpandConstant('{autopf}\{#MyAppName}');
  end;
end;

// 从PATH环境变量中移除指定的目录
function RemoveFromPath(Path: string; Directory: string): string;
var
  P: Integer;
  PathSeparator: string;
  PathValue: string;
begin
  Result := Path;
  PathSeparator := ';';
  PathValue := Path;
  
  // 处理环境变量形式的路径 %LDNS_HOME%
  P := Pos('%LDNS_HOME%', UpperCase(PathValue));
  if P > 0 then
  begin
    if P > 1 then
    begin
      // 如果前面有分号，也移除它
      if PathValue[P-1] = ';' then
        P := P - 1;
    end;
    
    // 计算要删除的长度
    Delete(PathValue, P, 12); // 12是 '%LDNS_HOME%'的长度
    
    // 如果后面有分号，也移除它
    if (P <= Length(PathValue)) and (PathValue[P] = ';') then
      Delete(PathValue, P, 1);
  end;
  
  Result := PathValue;
end;

const
  WM_SETTINGCHANGE = $001A;
  UninstallRegKey = '{#SetupSetting("AppId")}_is1';

// 读取已安装版本的卸载路径（从注册表 UninstallString）
function GetExistingUninstallerPath(): string;
var
  s: string;
begin
  s := '';
  if not RegQueryStringValue(HKLM, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + UninstallRegKey, 'UninstallString', s) then
    RegQueryStringValue(HKCU, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' + UninstallRegKey, 'UninstallString', s);
  if s <> '' then
  begin
    s := RemoveQuotes(s);
    // 如果包含参数，仅取可执行文件路径
    if Pos(' ', s) > 0 then s := Copy(s, 1, Pos(' ', s) - 1);
    Result := s;
    exit;
  end;
  Result := '';
end;

// 检查.NET 8.0运行时是否已安装
function IsDotNet8Installed: Boolean;
var
  ErrorCode: Integer;
  ResultCode: Integer;
  Output: AnsiString;
begin
  Result := False;
  if Exec('dotnet', '--list-runtimes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
  begin
    if ResultCode = 0 then
    begin
      if Exec('cmd.exe', '/c dotnet --list-runtimes | findstr "Microsoft.NETCore.App 8.0"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
      begin
        Result := (ResultCode = 0);
      end;
    end;
  end;
end;

// 准备安装时检查运行时与已安装状态
function InitializeSetup: Boolean;
var
  ResultCode: Integer;
  DotNetVersionFound: Boolean;
  ErrorMessage: string;
  RetryCount: Integer;
  ServiceExists: Boolean;
  UninsPath: string;
begin
  Result := True;
  ErrorMessage := '';
  
  try
    // 检查是否以管理员权限运行
    if not IsAdminLoggedOn then
    begin
      MsgBox('安装程序需要管理员权限才能正常运行。请右键点击安装程序并选择"以管理员身份运行"。', mbInformation, MB_OK);
      Result := False;
      Exit;
    end;
    
    // 通过注册表检测是否已安装
    UninsPath := GetExistingUninstallerPath();
    if UninsPath <> '' then
    begin
      // 尝试清理可能存在的服务残留
      Log('检测到已安装版本，检查是否有服务残留...');
      
      // 检查服务是否存在
      ServiceExists := False;
      if Exec(ExpandConstant('{cmd}'), '/C sc query LDNS', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
      begin
        ServiceExists := (ResultCode = 0);
      end;
      
      if ServiceExists then
      begin
        Log('发现服务残留，尝试停止并移除...');
        // 异步停止服务
        Exec(ExpandConstant('{cmd}'), '/C sc stop LDNS', '', SW_HIDE, ewNoWait, ResultCode);
        Sleep(1500);
        // 尝试删除服务
        Exec(ExpandConstant('{cmd}'), '/C sc delete LDNS', '', SW_HIDE, ewNoWait, ResultCode);
        Sleep(500);
      end;
      
      // 提示卸载
      if MsgBox('检测到已安装版本，是否尝试自动卸载旧版本？', mbConfirmation, MB_YESNO) = IDYES then
      begin
        if not Exec(UninsPath, '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-', '', SW_SHOW, ewWaitUntilTerminated, ResultCode) then
        begin
          MsgBox('卸载过程启动失败，请手动卸载旧版本后再安装。', mbError, MB_OK);
          Result := False;
          Exit;
        end;
        
        // 等待卸载完成
        Sleep(3000);
        
        // 再次检查是否仍存在
        UninsPath := GetExistingUninstallerPath();
        if UninsPath <> '' then
        begin
          MsgBox('自动卸载失败，请手动卸载旧版本后再安装。', mbError, MB_OK);
          Result := False;
        end;
      end
      else
      begin
        Result := False;
      end;
    end;
    
    // 检查.NET运行时
    if Result then
    begin
      DotNetVersionFound := False;
      RetryCount := 0;
      
      // 多次尝试检查，避免临时问题
      while (RetryCount < 3) and not DotNetVersionFound do
      begin
        try
          // 使用原始的IsDotNet8Installed函数进行检查
          if IsDotNet8Installed then
          begin
            DotNetVersionFound := True;
            Log('.NET 8.0运行时已找到。');
          end;
        except
          Log('检查.NET运行时出错，重试...');
        end;
        
        if not DotNetVersionFound then
        begin
          Sleep(500);
          RetryCount := RetryCount + 1;
        end;
      end;
      
      if not DotNetVersionFound then
      begin
        Log('警告：未检测到.NET 8.0运行时。');
        // 未找到.NET 8运行时，提示用户安装
        if MsgBox('检测到系统未安装.NET 8.0运行时，此应用程序需要.NET 8.0运行时才能正常运行。' + #13#10 + #13#10 +
                  '点击"是"将打开.NET 8.0运行时下载页面，请下载并安装后重新运行此安装程序。' + #13#10 +
                  '点击"否"将退出安装程序。', mbConfirmation, MB_YESNO) = IDYES then
        begin
          ShellExec('open', 'https://dotnet.microsoft.com/download/dotnet/8.0', '', '', SW_SHOWNORMAL, ewNoWait, ResultCode);
        end;
        Result := False;
        Exit;
      end;
    end;
    
    // 检查系统兼容性
    if Result then
    begin
      // 检查Windows版本
      if CompareVersion(GetWindowsVersion, '6.1') < 0 then
      begin
        MsgBox('此软件需要Windows 7或更高版本的操作系统。', mbError, MB_OK);
        Result := False;
      end;
    end;
  except
    on E: Exception do
    begin
      Log('InitializeSetup中的致命错误: ' + E.Message);
      MsgBox('安装程序初始化时发生错误: ' + E.Message, mbError, MB_OK);
      Result := False;
    end;
  end;
end;

// 安装完成后提示
procedure CurStepChanged(CurStep: TSetupStep);
var
  CurrentPath: string;
  ResultCode: Integer;
  i: Integer;
  bSuccess: Boolean;
  ServiceName: string;
  ServicePath: string;
begin
  if CurStep = ssPostInstall then
  begin
    CreateDir(ExpandConstant('{app}\Logs'));
    RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'LDNS_HOME', ExpandConstant('{app}'));
    CurrentPath := '';
    RegQueryStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', CurrentPath);
    if Pos(';%LDNS_HOME%;', ';' + CurrentPath + ';') = 0 then
    begin
      RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', CurrentPath + ';%LDNS_HOME%');
      SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
    end;
    
    // 如果选择了安装服务选项
    if WizardIsTaskSelected('installservice') then
    begin
      ServiceName := 'LDNS';
      ServicePath := ExpandConstant('{app}\LDNS.exe');
      bSuccess := False;
      
      // 尝试停止已存在的服务 - 使用异步方式避免阻塞
      Exec(ExpandConstant('{cmd}'), '/C sc stop ' + ServiceName, '', SW_HIDE, ewNoWait, ResultCode);
      // 给服务一些时间停止
      Sleep(1000);
      
      // 尝试删除已存在的服务
      Exec(ExpandConstant('{cmd}'), '/C sc delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
      
      // 等待服务完全删除
      Sleep(500);
      
      // 创建新服务
      Exec(ExpandConstant('{cmd}'), '/C sc create ' + ServiceName + ' binPath= "\"' + ServicePath + '\" --service" DisplayName= "本地DNS服务" start= auto', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
      
      // 添加描述
      Exec('sc.exe', 'description LDNS "本地DNS服务"', '', SW_HIDE, ewNoWait, ResultCode);
      
      // 启动服务并进行优化的重试机制
      i := 0;
      while i < 10 do // 减少重试次数但增加间隔
      begin
        // 尝试启动服务，使用异步方式避免长时间阻塞
        Exec(ExpandConstant('{cmd}'), '/C sc start ' + ServiceName, '', SW_HIDE, ewNoWait, ResultCode);
        
        // 等待服务有足够时间启动
        Sleep(1000);
        
        // 检查服务是否成功启动
        if Exec(ExpandConstant('{cmd}'), '/C sc query ' + ServiceName + ' | find "RUNNING"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
        begin
          bSuccess := True;
          break;
        end;
        
        // 指数退避策略，增加等待时间
        Sleep(500 * (i + 1));
        i := i + 1;
      end;
      
      // 如果服务启动失败，记录但不中断安装
      if not bSuccess then
      begin
        Log('Warning: Failed to start service after multiple attempts. Please start the service manually.');
      end;
    end;
  end;
end;

procedure KillProcess(ProcessName: string);
var
  ResultCode: Integer;
begin
  try
    // 使用taskkill命令强制终止进程
    Exec(ExpandConstant('{cmd}'), '/C taskkill /F /IM ' + ProcessName + ' >nul 2>&1', '', SW_HIDE, ewNoWait, ResultCode);
    // 等待进程终止
    Sleep(500);
  except
    // 忽略终止进程时的错误
  end;
end;

function ExecuteCommandWithTimeout(const Command, Parameters: string; Timeout: Integer): Boolean;
var
  ProcessInfo: TProcessInformation;
  StartUpInfo: TStartupInfo;
  ExitCode: Cardinal;
  TickCount: DWORD;
begin
  Result := False;
  
  // 初始化结构
  FillChar(StartUpInfo, SizeOf(StartUpInfo), 0);
  StartUpInfo.cb := SizeOf(StartUpInfo);
  StartUpInfo.dwFlags := STARTF_USESHOWWINDOW;
  StartUpInfo.wShowWindow := SW_HIDE;
  
  try
    // 创建进程
    if CreateProcess(ExpandConstant(Command), PChar(Parameters), nil, nil, False, 0, nil, nil, StartUpInfo, ProcessInfo) then
    begin
      try
        // 获取开始时间
        TickCount := GetTickCount;
        
        // 等待进程结束或超时
        while WaitForSingleObject(ProcessInfo.hProcess, 100) <> WAIT_OBJECT_0 do
        begin
          // 检查是否超时
          if (GetTickCount - TickCount) > Timeout then
          begin
            // 超时，尝试终止进程
            TerminateProcess(ProcessInfo.hProcess, 0);
            Log('Command execution timed out: ' + Command + ' ' + Parameters);
            Exit;
          end;
          
          // 允许UI响应
          Application.ProcessMessages;
        end;
        
        // 获取退出代码
        GetExitCodeProcess(ProcessInfo.hProcess, ExitCode);
        Result := (ExitCode = 0);
      finally
        // 关闭句柄
        CloseHandle(ProcessInfo.hProcess);
        CloseHandle(ProcessInfo.hThread);
      end;
    end;
  except
    // 记录异常但不中断
    Log('Exception executing command: ' + Command + ' ' + Parameters);
  end;
end;

procedure CleanupResources;
var
  ResultCode: Integer;
begin
  try
    // 尝试停止并删除服务
    Exec(ExpandConstant('{cmd}'), '/C sc stop LDNS >nul 2>&1', '', SW_HIDE, ewNoWait, ResultCode);
    Sleep(1000);
    Exec(ExpandConstant('{cmd}'), '/C sc delete LDNS >nul 2>&1', '', SW_HIDE, ewNoWait, ResultCode);
    
    // 终止可能仍在运行的进程
    KillProcess('LDNS.exe');
    
    // 清理环境变量
    RegDeleteValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'LDNS_HOME');
    
    // 清理PATH环境变量，移除%LDNS_HOME%
    var PathValue := '';
    var QueryResult := RegQueryStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', PathValue);
    if QueryResult then
    begin
      // 移除PATH中的%LDNS_HOME%
      PathValue := RemoveFromPath(PathValue, ExpandConstant('{app}'));
      RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', PathValue);
    end;
    
  except
    // 忽略清理时的错误
    Log('Warning: Error during resource cleanup');
  end;
end;

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
  ResultCode: Integer;
  ServiceExists: Boolean;
  PathValue: String;
  QueryResult: Boolean;
begin
  if CurUninstallStep = usUninstall then
  begin
    // 使用带超时的命令执行和资源清理
    CleanupResources;
    
    // 强制刷新环境变量
    SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
  end;
end;